Ana içeriğe geç
  1. 100 Günde SwiftUI Notları/

12.Gün - Swift Sınıflar (Class) ve Kalıtım (Inheritance)

Sınıflar, ilk bakışta Struct’a oldukça benzemektedir. Sınıflarla da kendisine ait property ve methodları olan yeni veri türleri oluşturabiliriz. Fakat bir fark ile Sınıflar bize kalıtım(inheritance) özelliğini de sunar. Kalıtım ile bir sınıfı diğer bir sınıfın temelleri üzerine inşa edebiliriz.

Struct’lar SwiftUI’de kullanıcı arayüz tasarımı yaparken yoğun olarak kullanılırlar. Veriler için de Sınıflar yoğun olarak kullanılmaktadır.

Sınıf (Class) Nasıl Oluşturulur? #

Struct ile kendi özel veri tipimizi oluşturmayı görmüştük. Kendi özel veri tipimizi oluşturmanın bir diğer yolu da Sınıflardır. Struct ile birçok ortak noktası olduğu gibi, çok önemli farkları da bulunmaktadır.

Sınıf ile Struct’ın Ortak Noktaları #

Sınıf ile Struct’ın Farklılıkları #

  • Bir sınıfı, başka bir sınıfın özellikleri üzerine inşa edebilir, tüm property ve methodlarını başlangıç noktası olarak alabiliriz. İstersek bazı methodları seçerek geçersiz de kılabiliriz.
  • İlk madde dolayısıyla, Sınıflar otomatik olarak memberwise initializer oluşturmazlar. Bu sebeple Sınıflar için custom initializer oluşturmamız ya da tüm property’ler için varsayılan değer atamamız gerekmektedir.
    • Memberwise initializer oluşturulamamasının sebebi kalıtımdır (inheritence). Kalıtım yoluyla bir sınıf oluşturduğumuzu düşünelim, ardından ana sınıfımda bazı property’lerde değişiklik yaptığımda, kalıtımı miras alan sınıfın initializer’ı bozulabilirdi.
  • Sınıfın bir instance’ını kopyaladığımızda her iki kopya da aynı verileri paylaşır, yani bir kopyayı değiştirirsek diğer kopya da değişir.
  • Bir Sınıfın instance’nın son örneği yok edildiğinde, Swift isteğe bağlı olarak deinitializer adı verilen özel bir fonksiyon çağırabilir.
  • Bir sınıfı sabit (constant) yapsak bile, değişken (var) oldukları sürece property’lerini değiştirebiliriz.

Sınıf (Class) Tanımı #

class Game {
    var score = 0 {
        didSet {
            print("Score is now \(score)")
        }
    }
}

var newGame = Game()
newGame.score += 10

Yukarıdaki class tanımı, struct tanımına oldukça benzemektedir. Fakat arka plandaki beş fark gerçekten çok önemlidir.

Kalıtım Yoluyla Miras Alma (Inheritance) #

Swift kalıtım (inheritance) yoluyla mevcut bir sınıfı temel alarak yeni bir sınıf oluşturmamızı sağlar. Bir sınıf, başka bir sınıftan (parent class veya super class) miras aldığında, Swift yeni sınıfa (child class veya subclass) parent sınıfın property ve methodlarına erişim vererek, child sınıfın özelliklerine ekleme ve değişiklikler yapmamıza olanak tanır.

Bir sınıfın diğerinden miras almasını sağlamak için, child olacak sınıfta : ile parent sınıfın adı yazılır. Örneğimizi inceleyelim;

//PARENT CLASS
class Employee {
    let hours: Int

    init(hours: Int) {
        self.hours = hours
    }
}

Employee sınıfından iki alt sınıf oluşturabiliriz. İki alt sınıfın her biri hours property’sine ve initializer’ına sahip olacaktır.

//CHILD CLASSES
class Developer: Employee {
    func work() {
        print("I'm writing code for \(hours) hours.")
    }
}

class Manager: Employee {
    func work() {
        print("I'm going to meetings for \(hours) hours.")
    }
}

Bu iki child sınıfın doğrudan hours property’sini kullanabildiğine dikkat edelim.

Bu child sınıflar, Employee ’den miras alır, ardından her bir child kendi özelleştirmelerini ekler. Dolayısıyla her birinin bir instance’ını oluşturup work() methodunu çağırırsak farklı bir sonuç elde ederiz.

let robert = Developer(hours: 8)
let joseph = Manager(hours: 10)
robert.work()
joseph.work()

//ÇIKTI:
//----------------------------------------
//I'm writing code for 8 hours.
//I'm going to meetings for 10 hours.

Child sınıf tarafından property’ler miras alınabildiği gibi, methodlar da miras alınabilir. Örneğin Employee sınıfına aşağıdaki methodu ekleyelim.

func printSummary() {
    print("I work \(hours) hours a day.")
}

Developer Employee ’den miras aldığı için, Developer instance’larının hepsinde printSummary() methodunu çağırmaya başlayabiliriz.

let novall = Developer(hours: 8)
novall.printSummary()

//ÇIKTI:
//----------------------------------------
//I work 8 hours a day.

Miras aldığımız bir methodu değiştirmek istediğimizde ise işler biraz karmaşık hale gelebilir. Örneğin, Employee sınıfına printSummary() methodunu koyduk ancak belki de child sınıflardan biri daha farklı davranmasını istiyor olabilir.

Bu noktada Swift basit bir kural ortaya koyar: Eğer bir child sınıf, parent sınıftaki bir methodu değiştirmek istiyorsa, child sınıfta override kullanılması gerekir. Bu iki şey yapar;

  1. override kullanmadan bir methodu değiştirmeye çalışırsak, Swift kodu oluşturmayı reddeder. Bu sayede bir methodu yanlışlıkla geçersiz kılmamış oluruz.
  2. override kullanıyorsak, ancak methodumuz aslında üst sınıftan bir şeyi geçersiz kılmıyorsa, Swift yine kodumuzu oluşturmayacaktır. Çünkü muhtemel hata yapmışızdır.

Dolayısıyla Developer sınıfının benzersiz bir printSummary() methoduna sahip olmasını istiyorsak, aşağıdaki kodu Developer sınıfına ekleriz.

override func printSummary() {
    print("I'm a developer who will sometimes work \(hours) hours a day, but other times spend hours arguing about whether code should be indented using tabs or spaces.")
}

override yapılırken bir ayrım da söz konusudur. Parent sınıfımızın parametre almayan bir work() methodu varsa, ancak alt sınıfın da String kabul eden bir work() methodu varsa bu durumda parent methodu değiştirmediğimiz için override kullanmamız gerekmez.

Sınıfımızın kalıtımı (inheritance) desteklememesi gerektiğinden eminsek, onu final olarak işaretleyebiliriz. Bu durumda, sınıfın kendisi başka sınıflardan miras alabilir fakat miras almak için kullanılamayacağı anlamına gelir. Hiçbir child sınıf final bir sınıfı parent olarak alamaz.

Sınıflar için Initializer Ekleme #

Sınıf initializer’ları struct initializer’larındna biraz daha karmaşıktır. Bir child sınıfın custom initializer’ı varsa kendi property’lerini ayarladıktan sonra mutlaka parent sınıfın initializer’ını çağırmalıdır.

Bir sınıf kalıtım yoluyla miras alsın veya almasın, otomatik olarak memberwise initializer Sınıflarda kullanılamaz. Dolayısıyla ya kendi custom initializer’ımızı yazmamız gerekir ya da sınıfın tüm property’lerine varsayılan değer ataması yapmamız gerekir.

Bir sınıf tanımlayarak başlayalım;

class Vehicle {
    let isElectric: Bool

    init(isElectric: Bool) {
        self.isElectric = isElectric
    }
}

Bu sınıfın tek bir Boolean property’si ve bu property’yi ayarlamak için bir initializer’ı var.

Şimdi Vehicle sınfından miras alarak bir Car sınıfı yapmak istedik;

//DİKKAT GEÇERSİZ KOD.
class Car: Vehicle {
    let isConvertible: Bool

    init(isConvertible: Bool) {
        self.isConvertible = isConvertible
    }
}

Ancak Swift yukarıdaki kodu reddedecektir. Vehicle sınıfının isElectric adında bir property’si var fakat bunun için bir değer sağlamadık.

Swift’in bizden istediği Car sınıfına isElectric ve isConvertible property’lerini içeren bir initializer sağlamamızdır. Fakat isElectric ’i kendimiz depolamak yerine onu aktarmamız gerekir. Yani parent sınıftan (Vehicle) kendi initializer’ını çalıştırmasını istememiz gerekir.

class Car: Vehicle {
    let isConvertible: Bool

    init(isElectric: Bool, isConvertible: Bool) {
        self.isConvertible = isConvertible
        super.init(isElectric: isElectric)
    }
}

super, Swift’in self ’e benzer şekilde bizim için otomatik olarak sağladığı değerlerden biridir. initializer gibi parent sınıfımıza ait methodları çağırmamızı sağlar. İstersek super ’i diğer methodlar için de kullanabiliriz, sadece init ile sınırlı değildir.

Artık her iki sınıfımızda da geçerli bir initalizer’a sahip olduğumuza göre, Car ’ın bir instance’ını şu şekilde oluşturabiliriz.

let teslaX = Car(isElectric: true, isConvertible: false)

Bir child sınıfın kendi intializer’ı yoksa, otomatik olarak parent sınıfın initializer’ını devralır.

Sınıflar Nasıl Kopyalanır? #

Swift’te sınıfın bir instance’ının tüm kopyaları aynı veriyi paylaşır yani, bir kopyada yaptığımız herhangi bir değişiklik otomatik olarak diğer kopyaları da değiştirir. Bunun nedeni Swift’te Sınıfların reference type olmasıdır.

Bunu uygulamada görelim;

class User {
    var username = "Anonymous"
}

User sınıfının sadece bir tane property’si vardır. Fakat bir Sınıfın içinde olduğundan tüm kopyalarda paylaşılacaktır.

User sınıfından bir instance oluşturalım;

var user1 = User()

Ardından user1 ’in bir kopyasını alalım ve kopya üzerinde username ’in değerini değiştirelim;

var user2 = user1
user2.username = "Taylor"

Şimdi de her iki instance’ın username’ini yazdıralım;

print(user1.username)  
print(user2.username)

//ÇIKTI:
//----------------------------------------
//Taylor
//Taylor

Instance’ın yalnızca birini değiştirsek de diğeri de değişti.

Bu bir hata gibi görülebilir fakat aslında bu önemli bir özelliktir. Bu sayede uygulamamızın genelinde ortak verileri kolayca paylaşabiliriz.

Sınıfların aksine Struct’larda instance kopyalarında veriler paylaşılmaz. Yani kodumuzda User sınıfını User Struct olarak değiştirirsek farklı bir sonuç elde ederiz. Bu durumda ekrana önce Anonymous ardından Taylor yazacaktır.

Deep Copy #

Bir sınıf instance’ının benzersiz bir kopyasını (unique copy) oluşturma işlemine deep copy denilmektedir. Deep copy yapılırken, yeni bir instance oluşturulur ve kopyalama işlemi bu şekilde yapılabilir.

class User {
    var username = "Anonymous"

    func copy() -> User {
        let user = User()
        user.username = username
        return user
    }
}

Yukarıdaki copy() methodu ile aynı başlangıç değerine sahip instance oluşturabiliriz. Bu sayede gelecekte yapılacak herhangi bir değişiklik orijinali etkilemeyecektir.

Referance Type ve Value Type #

Struct’lar, value type’dır. Yani verileri kendileri tutarlar, kaç tane property veya methodunun olduğu önemsizdir, yine de sabit bir değer gibi kabul edilirler. Diğer taraftan, Sınıflar ise reference type ‘dır. Yani başka bir yerde bulunan veriye atıfta bulunurlar.

var message = "Welcome"
var greeting = message
greeting = "Hello"

Açıkçası value type anlamak sezgisel olarak oldukça kolaydır. Yukarıdaki kod çalıştığında message hala “Welcome” olarak kalacak, sadece greeting ”Hello” olacaktır. Struct’lar değerleri tamamen değişkenlerinin içinde bulunur ve diğer değerlerle paylaşılmaz. Bu tüm verilerinin doğrudan depolandığı anlamına gelir, bu sebeple kopyalandıklarında tüm verilerin deep copy’sini almış oluruz.

Buna karşın, reference type’ı işaret tabelası gibi düşünebiliriz. Bir sınıfın instance’ını oluşturduğumuzda, instance’ı depolayan değişken aslında nesnenin kendisini değil, nesnenin varolduğu belleği işaret eder. Nesnenin bir kopyasını alırsak, yeni bir işaret tabelası elde ederiz ancak bu tabela hala orijinal nesnenin bulunduğu belleği işaret eder. Bir sınıfın bir örneğini değiştirmenin, tüm kopyaları değiştirmesinin nedeni budur. Nesnenin tüm kopyaları aynı bellek parçasına işaret eden tabelalardır sadece.

Sınıflarda Deinitializer #

Swift Sınıflarda isteğe bağlı olarak deinitializer oluşturulabilir. Deinitializer, nesne oluşturulduğunda değil, yok edildiğinde çağrılır.

Deinitializer özellikleri;

  1. Tıpkı initializer’larda olduğu gibi, deinitializer’larda da func kullanılmaz.
  2. Deinitializer’lar parametre almaz veya geri değer döndürmezler, () parantezler bile yazılmaz.
  3. Bir sınıfın instance’ının son kopyası yok edildiğinde deinitializer otomatik olarak çağırılır.
  4. Hiçbir zaman deinitializer’ı doğrudan çağıramayız; bunlar sistem tarafından otomatik olarak çağrılır.
  5. Struct’lar kopyalanamadıklarından, deinitializer’ları yoktur.

Deinitializer’ların tam olarak ne zaman çağrılacağı, ne yaptığımıza ve kapsamına (scope) bağlıdır. Kapsam (scope), bilginin mevcut olduğu bağlam (context) anlamına gelir. Kapsamı (scope) şu şekilde örneklendirebiliriz;

  • Bir fonksiyon içinde bir değişken oluşturursak, ona fonksiyonun dışından erişemeyiz.
  • Bir if koşulunun içinde bir değişken oluşturursak, bu değişken koşulun dışında kullanılamaz.
  • Döngü değişkeni (loop variable) da dahil olmak üzere, bir for döngüsü içinde bir değişken oluşturursak, bu değişkeni döngü dışında kullanamayız.

Bir değer kapsamdan(scope) çıktığında, içinde oluşturulduğu bağlamın (context) ortadan kalktığı anlamına gelir. Struct söz konusu olduğunda bu, verilerin yok edildiği anlamına gelir, ancak sınıflarda verilerin yalnızca bir kopyası yok edildiği anlamına gelir yani başka yerlerde hala başka kopyalar olabilir. Ancak son kopya yok olduğunda temel veri de yok edilir ve kullandığı bellek sisteme geri verilir.

Bunu göstermek bir örnek yapalım;

class User {
    let id: Int

    init(id: Int) {
        self.id = id
        print("User \(id): I'm alive!")
    }

    deinit {
        print("User \(id): I'm dead!")
    }
}

Döngü kullanarak, bu Sınıfın instance’larını hızlı bir şekilde oluşturup yok edebiliriz. Döngü içinde bir User instance oluşturursak, döngü yinelemesi (iteration) sona erdiğinde temel veri yok edilecektir.

for i in 1...3 {
    let user = User(id: i)
    print("User \(user.id): I'm in control!")
}

print("Loop is over!")

//ÇIKTI:
//----------------------------------------
//User 1: I'm alive!
//User 1: I'm in control!
//User 1: I'm dead!
//User 2: I'm alive!
//User 2: I'm in control!
//User 2: I'm dead!
//User 3: I'm alive!
//User 3: I'm in control!
//User 3: I'm dead!
//Loop is over!

Bu kod çalıştırıldığında, her bir user ’ın ayrı ayrı oluşturulduğu ve yok edildiğini göreceğiz. Yenisi oluşturulmadan önce diğeri tamamen yok edilecektir.

Deinitializer yalnızca bir sınıfın instance’ına kalan son referans yok edildiğinde çağrılır. Bu sakladığımız bir değişken, sabit ya da Array olabilir.

Örneğin; User instance’larını oluşturdukça ekliyor olsaydık, bunlar yalnızca dizi temizlendiğinde yok edilirdi.

var users = [User]()

for i in 1...3 {
    let user = User(id: i)
    print("User \(user.id): I'm in control!")
    users.append(user)
}

print("Loop is finished!")
users.removeAll()
print("Array is clear!")

//ÇIKTI:
//----------------------------------------
//User 1: I'm alive!
//User 1: I'm in control!
//User 2: I'm alive!
//User 2: I'm in control!
//User 3: I'm alive!
//User 3: I'm in control!
//Loop is finished!
//User 1: I'm dead!
//User 2: I'm dead!
//User 3: I'm dead!
//Array is clear!

Sınıfların İçindeki Değişkenlerle Çalışma (Mutability) #

Swift’in sınıfları işaret levhaları gibi çalışır: sahip olduğumuz bir sınıf instance’ının her kopyası aslında temel veri parçasına işaret eden bir işaret levhasıdır.

class User {
    var name = "Paul"
}

let user = User()
user.name = "Taylor"
print(user.name)

//ÇIKTI:
//----------------------------------------
//Taylor

Yukarıdaki kod, sabit(constant) User instance’ı oluşturur, ancak daha sonra onu değiştirir. Biraz garip görünüyor mi görünüyor?

Ancak sabit değeri hiç değiştirmez.Evet sınıfın içindeki veriler değişti, ancak sınıf instance’ının kendisi (oluşturduğumuz nesne) değişmedi ve aslında değiştirilemez çünkü onu sabit yaptık.

Şöyle düşünelim; bir user ’ı işaret eden, sabit (constant) bir tabela oluşturduk, ancak bu user ’ın isim etiketini sildik ve farklı bir isim yazdık. Söz konusu user değişmedi -hala var- ancak dahili verilerinin bir kısmı değişti.

Eğer name property’sini sabit(constant) yapsaydık, o zaman değiştirilemezdi.

Buna karşın, hem user instance’ını hem de name property’sini değişken (var) yapsak ne olur? Property’yi değiştirebiliriz, ayrıca istersek tamamen yeni bir User instance’ına da geçebiliriz. Tabela benzetmesine devam edecek olursak, bu tabelayı tamamen farklı bir kişiyi gösterecek şekilde çevirmek gibi olacaktır.

class User {
    var name = "Paul"
}

var user = User()
user.name = "Taylor"
user = User()
print(user.name)

//ÇIKTI:
//----------------------------------------
//Paul

Yukarıdaki kod sonuç olarak “Paul” yazdıracaktır, çünkü name ’i “Taylor” olarak değiştirsek de tüm user nesnesinin üzerine yeni bir tane yazarak onu “Paul” olarak resetledik.

Son varyasyonumuz, değişken bir instance ve sabit bir property’ye sahip olmaktır. Bu istersek yeni bir User oluşturabileceğimiz ancak property’sini yalnızca bir kez atayabileceğimiz manasına gelmektedir.

Elimizdeki dört olasılığı da değerlendirelim;

  1. Sabit (let) instance, sabit (let) property : her zaman aynı user ’a işaret eden, her zaman aynı name ’ e sahip tabela.
  2. Sabit (let) instance, değişken (var) property : her zaman aynı user ’ı işaret eden bir tabela, ancak name değişebilir.
  3. Değişken (var) instance, sabit (let) property : farklı user ’ları işaret edebilen tabela ancak name asla değişmez.
  4. Değişken (var) instance, değişken (var) property : farklı user işaret edebilen bir tabela ve bu user ’ların name ’leri de değişebilir.

Diyelim ki bize bir User instance verildi. Instance sabittir (let) ancak içindeki property değişken (var) olarak bildirilmiştir. Bu durum bize sadece istediğimiz zaman bu property’yi değiştirebileceğimizi değil aynı zamanda bu property’nin başka bir yerde değiştirilme olasılığı olduğunu da söyler : sahip olduğumuz sınıf başka bir yerden kopyalanmış olabilir ve property değişken (var) olduğu için kodun başka bir kısmı bunu sürpriz bir şekilde değiştirebilir.

Sabit (let) property’ler gördüğümüzde, ne mevcut kodumuzun ne de programın herhangi bir yerinin bu property’yi değiştirilemeyeceğinden emin olabiliriz. Ancak değişken (var) property’ler ile uğraştığımızda instance’ın sabit (let) olup olmadığına bakılmaksızın, verilerin değişebileceği olasılığı ortaya çıkar.

Bu Struct’lardan farklıdır, çünkü sabit struct’ların property’leri değişken yapılsa bile değiştirilemez. Çünkü struct’larda işaret levhası yoktur verilerini doğrudan tutar. Bu struct içindeki bir değeri değiştirmeye çalıştığımızda dolaylı olarak struct’ın kendisini de değiştirmiş olacağımız anlamına gelir ki bu da sabit olduğu için mümkün değildir.

İşte bu sebeplerle Sınıflarda mutating keywordünü kullanmaya gerek yoktur. Fakat struct’larda mutating oldukça önemlidir. Struct’larda mutating olarak işaretlenmiş methodlar ile property’lerinin değişebildiğini görmüştük, fakat struct’ın instance’ı sabit(let) olarak tanımlandıysa property’yi yine de değiştiremeyiz.

//AŞAĞIDAKİ KOD ÇALIŞMAYACAKTIR
struct Test {
    var name = "Anon"
    mutating func change(name2:String) {
        self.name = name2
    }
}
//BURADA let OLARAK TANIMLAMA YAPILDIĞINDAN DEĞİŞEMEZ.
let instance = Test()

instance.change(name2:"görkem")
print(instance.name)

Sınıflarda ise instance’ın nasıl oluşturulduğunun önemi yoktur (sabit veya değişken). Bir property’nin değiştirilip değiştirilemeyeceğini belirleyen tek şey, property’nin kendisinin bir sabit olarak oluşturulup oluşturulmadığıdır. Swift property’nin nasıl oluşturulduğuna bakarak değişip değişmeyeceğini anlayabilir özel olarak işaretlemeye gerek yoktur.

100 Days of SwiftUI Checkpoint - 7 #


Bu yazıyı İngilizce olarak da okuyabilirsiniz.
You can also read this article in English.

Bu yazı, SwiftUI Day 12 adresinde bulunan yazılardan kendim için aldığım notları içermektedir. Orjinal dersi takip etmek için lütfen bağlantıya tıklayın.